# -*- coding: utf-8 -*-
"""
Created on Sat Jul 09 05:20:34 2011

@author: Shpiz

Requires : 
    jinja2  http://jinja.pocoo.org/,
    argparse ,
    watchdog http://pypi.python.org/pypi/watchdog/0.5.4
    guidata http://pypi.python.org/pypi/guidata/
    
    if you dont have these, they are easy_installable
    
    jinja is used only for the 2 functions:
        meta.find_referenced_templates
        meta.find_undeclared_variables
        
    and for the Environment and FileSystemLoader classes
    that are needed for meta. 
    as jinja is a superset of django, this should work 
    well for django as well
    
    easy_install watchdog
    easy_install guidata
    easy_install jinja2
    
"""
changes_fixes =\
"""
id  change
1   removed TEMPLATE_PATH, added *source because of bug discovered when reading 
    templates with macros and '' signs. for instance the following template would fail
    {% macro input(name, value='', type='text') -%}
    <input type="{{ type }}" value="{{ value|e }}" name="{{ name }}">
    {%- endmacro %}
"""
mysite = "http://code.google.com/p/tpl2cls/"
import datetime,time,re,os
from jinja2 import Environment, FileSystemLoader,meta,TemplateError

MAIN_TEMPLATE = \
"""
{% macro class_macro(class)-%}
class {{class.name}}:
    {%if class.is_folder%}
    '''
    represents the {{class.folder}} folder. 
    exists merely for namespacy convenience.
    '''
    is_folder = {{class.is_folder or False}}
    {% for child in class.children %}
    {{class_macro(child)|indent(4)}}  
    {%endfor%}
    {%else%}
    '''
    represents the {{class.filename}} template
    {%if class.exception %}
    NOTE: the {{class.filename}} template is unrenderable and
    will raise an error if render is used.
    {%endif%}
    '''
    
    filename = '{{ class.filename  }}'
    tpl = None
    exception_string = {% if class.exception %} ''' {{class.exception}} '''{% else %} None {%endif%}
    is_folder = {{class.is_folder or False}}
    @classmethod
    def render(cls,{%for param in class.params%}{{param}},{%endfor%}   **kwargs):
        if not cls.tpl or DEBUG:
            cls.tpl = env.get_template(cls.filename)
        tpl_kwargs = { {%for param in class.params%} '{{param}}':{{param}}{% if not loop.last %},{%endif%}{%endfor%} }
        tpl_kwargs.update(kwargs)
        return cls.tpl.render(**tpl_kwargs)
        
    @classmethod
    def render_loose(cls,{%for param in class.params%}{{param}} = None,{%endfor%}   **kwargs):
        if not cls.tpl or DEBUG:
            cls.tpl = env.get_template(cls.filename)
        tpl_kwargs = { {%for param in class.params%} '{{param}}':{{param}}{% if not loop.last %},{%endif%}{%endfor%} }
        tpl_kwargs.update(kwargs)
        return cls.tpl.render(**tpl_kwargs)
        
    @classmethod
    def reset(cls):
        cls.tpl = None

    
classes['{{ class.filename }}'] = {{class.name}}
    {%endif%}
{%- endmacro %}

#Automatically generated file
# DO NOT MODIFY
# CREATION TIME :
#{{time}}
import os
{% if env_hook %}
from {{env_hook_module }} import {{env_hook.func_name}}
{% endif %}
from jinja2 import Environment, FileSystemLoader,\
    TemplateError,UndefinedError,TemplateNotFound,\
    TemplatesNotFound,TemplateSyntaxError,TemplateAssertionError
TEMPLATE_PATH = {{rel_path}}
loader = FileSystemLoader(TEMPLATE_PATH)
env = Environment(loader=loader)
{% if env_hook %}
{{env_hook.func_name}}(env)
{% endif %}

DEBUG = {{debug}}
classes = {}
def reset(clss):
    '''
    resets collection of classes
    '''
    for cls in clss:
        cls.reset()
    
def reset_all():
    '''
    resets all classes
    '''
    reset(classes.values())
    


{{class_macro(class)}}





"""
class Meta:
    is_inited = False
    @classmethod
    def init(cls,loader,env=None):
        cls.is_inited = True
        cls.env = env or Environment(loader)
        cls.loader = loader
        
    "convenience class for getting undeclared params of file"
    @classmethod
    def get_undeclared_params(self,filename):
        "convenience for building ast from file with Env and Loader"
        return meta.find_undeclared_variables(Meta.ast(filename))
    @classmethod
    def ast(self,filename):
        "returns ast from filename"
        source = Meta.loader.get_source(Meta.env,filename)
        return Meta.env.parse(*source)#id=1 
    
    @classmethod
    def get_all_undeclared_params(self,filename):
        "gets all undeclared param (including those extended by the file)"
        filename = filename.replace('\\','/')
        filenames = [filename,]
        ret = set()
        while filenames:
            filename = filenames.pop(-1)
            ast = Meta.ast(filename)
            ret.update(Meta.get_undeclared_params(filename))
            for fname in meta.find_referenced_templates(ast):
                if fname is not None:
                    filenames.append(fname) 
        odd_list = filter(lambda x:x in self.env.globals,
                          ret)
        if odd_list:
            print """

#===============================================================================
#         %s
#===============================================================================
%s
        """ % (filename, odd_list)
        
        print self.env.globals
        ret -= set(odd_list)#globals are ignored
        print ret
        
        return ret 
        
    @classmethod
    def file2cls_name(self,fname):
        return fname.replace('.','_').replace('/','_').replace(' ','_')
    


def build_file_content(rel_path, tpl_path, debug, ignore_lst =[], use_lst=['html'],
                       env_prepare_hook = None, hook_module = None):
    ldr = FileSystemLoader(tpl_path)    
    env = Environment(loader = ldr)
    if env_prepare_hook:
        env_prepare_hook(env)
    Meta.init(ldr,env)
#    print env.list_templates()
    env =  Environment(loader = FileSystemLoader(\
                os.path.abspath(tpl_path)))#id=1
    os.chdir(tpl_path)
    base_class = get_class_from_path('',ignore_lst,use_lst)
    print base_class    
    tpl = env.from_string(MAIN_TEMPLATE)
    print rel_path
    rel_path_str = "os.sep.join([%s] + %r)"%\
                        ("os.path.dirname(__file__)",
                        rel_path.split(os.sep))
                         
    content = tpl.render({'class':base_class,
                          'debug':debug,
                          'rel_path':rel_path_str,
                          'time':datetime.datetime.now(),
                            'env_hook': env_prepare_hook,
                            'env_hook_module': hook_module
})
    return content
    
import re
def get_class_from_path(path,ig_lst=[],use_lst=['html','htm']):
    abspath = os.path.abspath(path)
    print 'PATH',path
    ret = {'name':Meta.file2cls_name(os.path.basename(abspath))}
    ret['is_folder'] = os.path.isdir(abspath)
    if ret['is_folder']:
        lst = os.listdir(abspath)
        map_func = lambda file_or_dir: \
            get_class_from_path(os.path.join(path,file_or_dir))
        lst = map(map_func,lst)
        ret['children'] = filter(None, lst)
    else:
        head,ext = os.path.splitext(abspath)
        ext = ext.replace('.','')
#        print ext
#        print map(lambda i: re.match(i,ext),ig_lst)
#        print map(lambda i: re.match(i,ext),use_lst)
        if any(map(lambda i: re.match(i,ext),ig_lst)):
            return
        if not any(map(lambda i: re.match(i,ext),use_lst)):
            return
        ret['filename'] = path.replace('\\','/')
        try:
            ret['params'] = Meta.get_all_undeclared_params(path)
        except Exception as e:
            print e
            print repr(e)
            ret['exception'] = 'TemplateError("' + str(type(e)) + ':' + str(e) +'")'
#            print repr(e),e
            
    return ret
        
        
    

    
############
###Watchdog#
############
def watch(path,handler):
    """
    watches the path forever, calling the handler if 
    anything changes.
    allows keyboard interrupt
    """
    from watchdog.observers import Observer
    
    observer = Observer()
    observer.schedule(handler, path, recursive=True)
    observer.start()
    try:
        while True:
            time.sleep(0.7)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()
    
from watchdog.events import FileSystemEventHandler    


class MyHandler(FileSystemEventHandler):
    """
    handles changes to files. ignoring certain paths and calling 
    func() on any change
    """
    last_time = time.clock()
    last_file_name = None
    
    def __init__(self,min_interval,ignore_paths,use_paths,func):
        self.min_interval = min_interval
        self.ignore_paths = ignore_paths
        self.func = func
        self.use_paths = use_paths
        
    def on_modified(self, event):
        if time.clock() - self.last_time < self.min_interval:
            return
        if event.is_directory:
            return
#            if event.src_path == self.last_file_name:
#                return
        fname = os.path.basename(event.src_path)
        lst_ig = [re.search(x,fname) for x in self.ignore_paths]
        lst_use = [re.search(x,fname) for x in self.use_paths]        
        
        if any(lst_ig):
            return
        if not any(lst_use):
            return
        self.last_time = time.clock()
        self.last_file_name = event.src_path
        print 'Event:',str(datetime.datetime.now())
        print 'Last changed file:',self.last_file_name
        try:
            self.func()
            print 'Update Done'
        except Exception as e:
            print 'Update Failed',e


################
###End Watchdog#
################
   
######################
####Command Line args#
######################
def run_from_command_line():
    import argparse
    import os
    parser = argparse.ArgumentParser(description='turn template folder to a '\
        +'python file with classes representing templates',\
        epilog ="if no args are present will run GUI. "+\
        "\nFor more info visit "+mysite)
    parser.add_argument('-tp', dest ='tpl_path',metavar='PATH', type=str, nargs='*',\
                        help='the template path',default =['templates'])
    parser.add_argument('-o',dest ='outpath',metavar='OUTFILE',type=str ,nargs ='*',\
        help='file that will hold script. (should end in .py)',\
        default=['tplcls.py'])
    parser.add_argument('-d',dest='debug',nargs ='?',\
        help='debug flag',const=True,default = False)
    parser.add_argument('-interval',dest='interval',nargs ='?',type=float,\
        help='min interval between reruns. recommended= 5',default = 5)
    parser.add_argument('-ignore',dest='ignore_lst',nargs ='+',type=str,\
        help='list of files or re to ignore',default = [r'py.'])
    parser.add_argument('-useonly',dest='use_lst',nargs ='+',type=str,\
        help='list of files or re to use',default = [r'html',r'htm'])
    parser.add_argument('-w',dest='watch',nargs ='?',\
        help='use watchdog',const=True,default = False)
    args = parser.parse_args()
    tpl_path = " ".join(args.tpl_path)
    outpath = " ".join(args.outpath)
    tpl_path = os.path.abspath(tpl_path)
    outpath = os.path.abspath(outpath)
    args.ignore_lst.append(outpath)
    ignore_lst = args.ignore_lst
    use_lst = args.use_lst
    watch = args.watch
    interval = args.interval
    debug = args.debug
    run(tpl_path,outpath,debug,ignore_lst,use_lst,interval,watch)


##########################
####End Command Line args#
##########################

def run(tpl_path,outpath,debug,ignore_lst,use_lst,interval,watchdog,
        env_prepare_hook = None, hook_module = None) :
    def func():
        print tpl_path,outpath
        rel_path = os.path.relpath(tpl_path, os.path.dirname(outpath))
        
        print 'rel_path = ', rel_path
        content = build_file_content(rel_path,tpl_path,debug,ignore_lst,use_lst,
                                     env_prepare_hook, hook_module)
        f = open(outpath,'w')
        f.write(content)
        f.close()
        if debug:
            print 'Done'
            print 'Checking Import:'
            try:
                __import__(os.path.basename(outpath).replace('.py',''))
                print 'OK'
            except Exception as e:
                print 'IMPORT FAILED',e
    func()
    if watchdog:
        handler = MyHandler(interval,ignore_lst,use_lst,func)
        watch(tpl_path,handler)
    
        
##################
########GUI#######
##################


def run_from_gui():
    
    import guidata
    import guidata.dataset.datatypes as dt
    import guidata.dataset.dataitems as di
    app = guidata.qapplication()
    class Tpl2ClsGui(dt.DataSet):
        """Tpl2Cls:
            lorem ipsum
        """
        ignore_lst = di.StringItem('Files to Ignore ',\
            help = 'space separated file extensions(interperted as py regexes).  '\
            + 'this means that special chars may need to be escaped',\
            default = "py pyc pyo")
        use_lst = di.StringItem('Files to Use',\
            help = 'space separated file extensions(interperted as py regexes).  '\
            + 'this means that special chars may need to be escaped',\
            default = "html htm tpl txt")
            
        tpl_path = di.DirectoryItem("Directory", help = 'The template path')
        outpath = di.FileSaveItem("Generated file location", "py", 'tplcls.py')
        interval = di.IntItem('Interval between generations',default = 5,\
                help= 'min interval between reruns of gen script. recommended= 5')
        watchdog = di.BoolItem("run the Watchdog",default=True,help=\
            "enable this to check when files change and regen module")
        debug = di.BoolItem("verbose",default=True)
        logfile = di.FileSaveItem("Log file ", "log", 'tplcls.log')

    param = Tpl2ClsGui()
    res = param.edit()
    if not res:
        print 'Exiting'
        return
    class DummyPrinter:
        filename = param.logfile
        file = None
        def write(self,msg):
            self.file = self.file or open(self.filename,'a+')
            self.file.write(msg)
            self.file.close()
            self.file = None
    import sys
#    sys.stdout = DummyPrinter()
    transform_list = (lambda func :\
            (lambda string:map(func,string.split())))(lambda s:s)
            
    param.ignore_lst = transform_list(param.ignore_lst)
    param.use_lst = transform_list(param.use_lst)
    run(param.tpl_path,param.outpath,param.debug\
    ,param.ignore_lst,param.use_lst,param.interval,param.watchdog)
    
################
#####END GUI####
################


if __name__=='__main__':
    print 'Start'
    import sys 
    print sys.argv
    if not sys.argv[1:]:
        print 'Running Gui'
        run_from_gui()
    else:
        print 'Not Running Gui'
        run_from_command_line()

